接下來要建立 datasource system,讓 Agent 未來可以使用不同類型的資料來源。
跟這個系列前面的內容一樣,這裡沒有唯一正確的做法。比較像是我自己的設計習慣,以及我希望這個 system 最後可以長成什麼樣子。
對我來說,我比較喜歡 start from the end:先想清楚最後希望 system 怎麼被使用,再一步一步往那個方向做。
例如:
核心想法很簡單:
新增 datasource 時,盡量少改動既有的 system。
不過在開始寫 CSV reader 之前,我想先建立整個 application 之後都會使用的 config layer。
目前 datasource 是第一個需要設定的部分,所以先從它開始。
我希望目前的 config 先保持很簡單:
data:
path: data/youtube_trending_cleaned_us.csv
之後如果 system 增加其他需要設定的部分,也可以繼續加到同一份 config 裡。
例如未來可能會有:
data:
...
database:
...
sandbox:
...
但目前還沒有需要,所以先不加。
另外,這裡不會另外設定 format。
file format 直接從 path 的 extension 判斷,避免同一件事情出現兩個 source of truth。
這是我給 coding agent 的 prompt:
Add a config layer.
config.yml at the repo root, one section per concern. For now just:
data:
path: data/youtube_trending_cleaned_us.csv
A pydantic schema under config/, split by section:
- config/data.py — DataType enum (CSV only) and DataConfig
- config/base.py — the top-level Config, one field per section,
with a load() classmethod that reads the yml and validates it
- config/__init__.py — cfg = Config.load(), which the app imports
The data format is not a config field — it follows from the path's
extension. The enum exists so code can name a format without a magic
string. An unsupported extension must fail when the config loads, not
when something first reads the file.
Reject unknown keys.
Keep it minimal — no settings the app doesn't read, no options nothing
uses, no abstraction for a single case.
Test that the yml parses into the schema, that an unknown format is
rejected at load time, and that format can't be set in the yml.
目前 config 相關的結構如下:
config.yml
config/
├── __init__.py
├── base.py
└── data.py
config/base.py 負責整份 application config。
config/data.py 則只處理 datasource 相關設定。
目前只有一個 section:data。
config.ymlrepo root 的設定如下:
data:
path: data/youtube_trending_cleaned_us.csv
目前只需要提供 data path。
config/data.py先定義 application 目前支援的 data format:
from enum import Enum
from pathlib import Path
from pydantic import BaseModel, ConfigDict, model_validator
class DataType(str, Enum):
CSV = "csv"
@classmethod
def from_path(cls, path: Path) -> "DataType":
suffix = path.suffix.lower().lstrip(".")
try:
return cls(suffix)
except ValueError:
raise ValueError(
f"unsupported data format {suffix!r} for {path}"
) from None
例如:
DataType.from_path(Path("data.csv"))
會得到:
DataType.CSV
如果是目前不支援的格式,例如:
DataType.from_path(Path("data.pdf"))
則會直接失敗。
接著是 DataConfig:
class DataConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
path: Path
@model_validator(mode="after")
def _check_format(self) -> "DataConfig":
DataType.from_path(self.path)
return self
@property
def format(self) -> DataType:
return DataType.from_path(self.path)
這裡主要做兩件事:
config/base.py接著是 top-level config:
from pathlib import Path
import yaml
from pydantic import BaseModel, ConfigDict
from .data import DataConfig
class Config(BaseModel):
model_config = ConfigDict(extra="forbid")
data: DataConfig
@classmethod
def load(cls, path: Path | str = "config.yml") -> "Config":
return cls.model_validate(
yaml.safe_load(Path(path).read_text())
)
這裡負責讀取整份 config.yml,並在 application 啟動時一起完成 validation。
之後如果有新的 config section,也可以繼續加到這裡。
config/__init__.py最後:
from .base import Config
cfg = Config.load()
application 其他地方就可以直接使用:
from config import cfg
print(cfg.data.path)
print(cfg.data.format)
這樣 config 的讀取與 validation 都集中在同一個地方,不需要每個 module 自己處理。
到這裡,我們還沒有真的開始寫 CSV reader。
但 application 已經有一個可以繼續擴充的 config layer,而且 datasource 也有了最基本的設定。
下一步就可以直接使用:
cfg.data.path
cfg.data.format
開始建立真正的 file reader。